本來在幻想,如果WebAssembly程式要做出Console程式的功能,那只要用一個Memory做標準輸出,一個做標準輸入,還有一個做錯誤輸出不就好了?不過看了一下規格跟指令...雖然記憶體區塊可以加上Identifier(就是之前在程式中錢號$開頭的名字,不過這只是個語法糖就是了,編譯時這些都會變成索引),但是指令的語法裡面沒有...看起來就是只能有一個記憶體區塊的樣子XD
所以,雖然可以同時由Javascript端,以及WebAssembly程式端寫入及讀出Memory,但是需要做一下記憶體的規劃
簡單的做法,就是透過importObject,讓WebAssembly程式的Instance可以透過輸入global變數的方式,設定好in/out使用記憶體的基底。例如在importObject中這樣指定:
let importObject = {
js: {
inBase: 0,
outBase: 1024
}
};
然後WebAssembly程式這樣寫:
(module
(global $outbase (import "js" "outBase") i32)
(global $inbase (import "js" "inBase") i32)
...
(data (get_global $outbase) "Hello, ")
)
這樣就可以知道要從$inBase開始的基底位址讀入輸入的資料,然後從$outBase開始的基底位址寫入資料。例如底下的資料區段,就可以設定成從$outBase開始填入資料。
因為程式會依賴從import輸入的資料,所以wabt在編譯時會檢查使用到import的指令以及區段是否在其他區段的前面。
這其實是很基本的網頁程式,就是從文字輸入框輸入一個名字,按下submit按鈕,然後出現一段招呼的訊息。
網頁端程式:
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
Name: <input id="in_name" type="text" /><button id="btn_submit" onclick="say();return false">Submit</button><br><br>
<div id="panel"></div>
<script src="../wasm_util.js"></script>
<script>
let out = new WebAssembly.Memory({initial: 1});
let importObjects = {
js: {
out: out,
log: log,
outBase: 1024,
inBase: 0
}
}
let ginstance = null;
let s = new Wasm('test005.wasm')
.getInstance(importObjects);
function say(obj) {
if(ginstance === null) {
s
.then(instance => {
process(ginstance=instance);
});
} else {
process(ginstance);
}
}
function process(instance) {
let name = document.getElementById('in_name').value;
name.split('').forEach(s => console.log(s.charCodeAt(0)));
let view = new Uint8Array(out.buffer, 0, name.length);
for(let i=0; i<name.length; i++) {
view[i] = name.charCodeAt(i);
}
console.log(view);
instance.exports.hello(0, name.length);
}
function log(offset, length) {
let view = new Uint8Array(out.buffer, offset, length);
console.log(view);
document.getElementById('panel').innerHTML = String.fromCharCode.apply(null, view);
}
</script>
</body>
</html>
先用簡單的String.prototype.charCodeAt()
把字串轉成Uint8Array,然後用String.fromCharCode()
把Uint8Array轉成字串。
WebAssembly程式如下:
(module
(func $log (import "js" "log") (param i32) (param i32))
(memory (import "js" "out") 1)
(global $outbase (import "js" "outBase") i32)
(global $inbase (import "js" "inBase") i32)
(func (export "hello") (param $start i32) (param $len i32)
(local $idx_in i32)
(local $idx_out i32)
(local $i i32)
(set_local $idx_in (i32.add (get_global $inbase) (get_local $start)))
(set_local $idx_out (i32.add (get_global $outbase) (i32.const 7)))
(set_local $i (i32.const 0))
(block $break
(loop $while1
(br_if $break (i32.eq (get_local $i) (get_local $len)))
(i32.store8 (get_local $idx_out) (i32.load8_u (get_local $idx_in)))
(set_local $i (i32.add (i32.const 1) (get_local $i)))
(set_local $idx_in (i32.add (get_local $idx_in) (i32.const 1)))
(set_local $idx_out (i32.add (get_local $idx_out) (i32.const 1)))
(br $while1)
)
)
(call $log (get_global $outbase) (i32.add (i32.const 7) (get_local $len)))
)
(data (get_global $outbase) "Hello, ")
)
指令多的時候,大腦會Stack Overflow,所以改用S-Expression的語法來寫,這樣比較不需要去想現在指令用的參數在堆疊的哪裡XD
這裡使用了幾個流程控制的指令,包含block、loop、br_if還有br。主要其實只是在控制一個迴圈,來逐個把資料從Memory中的輸入資料的位置,複製到輸出資料的位置。
goto $label
所以上面程式中,br $while1
會跳到迴圈開頭重新執行,而br_if $break
會離開區塊,也就跳離迴圈。
然後可以試試看,輸入fillano
,按下submit按鈕,頁面會出現Hello, fillano
:
不過...如果輸入中文,會跑出亂碼:
因為輸入時直接用charCodeAt()取出直接塞Uint8Array,多出的位元都被截掉了XD。網頁有指定編碼是UTF-8,但是不想自己寫UTF-8轉換成Uint8Array的程式,所以上網找了一個:
JavaScript UTF-8 encoding and decoding with TypedArray
把他加到網頁,然後調整一下網頁的程式:
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
Name: <input id="in_name" type="text" /><button id="btn_submit" onclick="say();return false">Submit</button><br><br>
<div id="panel"></div>
<script src="../wasm_util.js"></script>
<script src="../utf8.js"></script>
<script>
let out = new WebAssembly.Memory({initial: 1});
let importObjects = {
js: {
out: out,
log: log,
outBase: 1024,
inBase: 0
}
}
let ginstance = null;
let s = new Wasm('test006.wasm')
.getInstance(importObjects);
function say(obj) {
if(ginstance === null) {
s
.then(instance => {
process(ginstance=instance);
});
} else {
process(ginstance);
}
}
function process(instance) {
let name = document.getElementById('in_name').value;
let enc = encodeUTF8(name);
let view = new Uint8Array(out.buffer, 0, enc.length);
for(let i=0; i<enc.length; i++) {
view[i] = enc[i];
}
console.log(view);
instance.exports.hello(0, enc.length);
}
function log(offset, length) {
let view = new Uint8Array(out.buffer, offset, length);
console.log(view);
document.getElementById('panel').innerHTML = decodeUTF8(view);
}
</script>
</body>
</html>
WebAssembly端的程式就不需要改了,因為邏輯沒變。
執行看看,輸入中文也可以正確顯示了:
關於Memory的部份先嘗試到這裡,後面如果用到再來深究。明天先來看看怎麼使用Table。